#include "ET++.ph"
#ifdef __GNUG__
#pragma implementation
#endif

#include "ShellTView.h"

#include "Class.h"
#include "Iterator.h"
#include "OrdColl.h"
#include "ByteArray.h"
#include "StyledText.h"
#include "System.h"
#include "String.h"
#include "Alert_e.h"
#include "Menu.h"
#include "MenuBar.h"
#include "Window.h"
#include "WindowSystem.h"
#include "Math.h"
#include "PttyConnection.h"

const char *gDefaultArgv[] = {
	"/bin/csh",
	0
};

const int
	cPttyBufSize    = 8192,
	cHighWaterMark  = 50000,
	cLowWaterMark   = 4000,
	cESC            = 27;

//---- output cursor (upside-down caret)

#include "revcaret.xpm3"
static SmartBitmap gRevCaretImage(revcaret);

static Ink *gCaretInk;

//---- ShellConnection ---------------------------------------------------------

class ShellConnection : public PttyConnection {
	ShellTextView *tv;
public:
	ShellConnection(ShellTextView *t) : PttyConnection()
		{ tv= t; }
	void InputNotify()
		{ tv->Received(); }
	void DiedNotify()
		{ tv->SlaveDied(); }
};

//---- ShellTextView -----------------------------------------------------------

NewMetaImpl(ShellTextView, TextView, (TP(outputCursor), TP(inputCursor),
		TB(pendingCR), TB(doReveal), TP(ptty), TP(noEchoBuffer),
		T(lastSubmit), T(maxLogSize), T(state), TP(pttycstr), TSP(argv)));
						
ShellTextView::ShellTextView(EvtHandler *eh, const Rectangle &r, Text *t,
	const char *name, const char **args, bool cons, TextViewFlags fl,
		const Point &b) : TextView(eh, r, t, TRUE, fl, b, eTViewLeft)
{
	Init(TRUE, name, args, cons);
}

void ShellTextView::Init(bool reveal, const char *name, const char **args, bool cons)
{
	doReveal= reveal;
	outputCursor= new Mark(0, 0, eStateNone, (eMarkFlags)eMarkNone);
	inputCursor= new Mark(0, 0, eStateNone, (eMarkFlags)eMarkNone);

	linebuf= 0;
	pttycstr= 0;
	pendingCR= inReceive= FALSE;
	maxLogSize= cHighWaterMark;
	noEchoBuffer= new GapText(20);
	inpQueue= new OrdCollection;
	GetText()->AddMark(outputCursor);
	GetText()->AddMark(inputCursor);
	gSystem->Setenv("TERM", GetTerm());
	gSystem->Setenv("TERMCAP", GetTermCap());
	ptty= new ShellConnection(this);
	if (name)
		Start(name, args, cons);
}

ShellTextView::~ShellTextView()
{
	Term();
}

void ShellTextView::Term()
{
	if (text) {
		Object *op= text->RemoveMark(outputCursor);
		SafeDelete(op);
		op= text->RemoveMark(inputCursor);
		SafeDelete(op);
	}
	PerformCommand(gResetUndo);
	SafeDelete(pttycstr);
	SafeDelete(noEchoBuffer);
	SafeDelete(inpQueue);
	SafeDelete(ptty);
}

Command *ShellTextView::DoKeyCommand(int ch, Token token)
{
	if (!ptty->IsRunning())
		return gNoChanges;

	if (ModifiesReadOnly())
		SetSelection(text->End(), text->End(), TRUE);
	if (ptty->IsRawOrCBreak()) {
		char c= ch;
		ptty->Write(&c, 1);
		lastSubmit= text->End();
		if (ptty->IsEcho())
			return TextView::DoKeyCommand(ch, token);
		return gResetUndo;
	}
	if (strchr(pttycstr, ch)) {
		byte *cbuf;
		int n;

		if (ch == ptty->CCIntr() || ch == ptty->CCQuit()) {
			SubmitInterrupt(ch);
			return gResetUndo;
		}

		GapText *buf= new GapText(4096), *tosend;                
		if (ptty->IsEcho()) {
			buf->ReplaceRange(0, buf->End(), text, outputCursor->Pos(), text->End());
			tosend= buf;
		} else
			tosend= noEchoBuffer;
		tosend->AddChar(tosend->End(), ch);
		n= tosend->End();
		cbuf= tosend->GetLineAccess(&linebuf, 0, n);
		if (tosend == noEchoBuffer)
			noEchoBuffer->Empty();
		Submit((char*)cbuf, n);
		delete buf;
		return gNoChanges;
	}
	if (!ptty->IsEcho()) {
		noEchoBuffer->Append(ch);
		return gNoChanges;
	}
	if (text->IsKindOf(StyledText) && ch != '\b') {
		int f, t;
		GetSelection(&f, &t);
		StyledText *st= (StyledText*) text;
		st->SetCharStyle(eTxTPFace, f, t,
			CharStyleSpec(eFontDefault, eFaceBold, 0, 0, FALSE));
	}
	if (Iscntrl(ch) && ch != '\b' && ch != '\t')
		return CntrlChar((byte)ch);
	return TextView::DoKeyCommand(ch, token);
}

const char *ShellTextView::GetTermCap()
{
	return "etterm|etterm dumb terminal emulator:bs";
}

const char *ShellTextView::GetTerm()
{
	return "etterm";
}

void ShellTextView::Received()
{
	char buf[cPttyBufSize];
	int n= 0, f, t, upto;

	Focus();
	inReceive= TRUE;
	GetSelection(&f, &t);
	inputCursor->ChangeMark(f, t-f);
	outputCursor->Unlock();

	if (ptty->IsRunning())
		n= ptty->Read(buf, sizeof(buf)-1);
		
	if (n <= 0)     // EOT from child or lost connection
		SlaveDied();
	else {
		GapText *gt= new GapText(n);
		HideSelection(FALSE);
		SetNoBatch(ptty->IsRawOrCBreak());
		ProcessOutput(gt, buf, n);
		if (outputCursor->Pos() < lastSubmit)
			upto= Math::Min(lastSubmit, text->End());
		else
			upto= outputCursor->Pos();
		InsertReceivedText(gt, outputCursor->Pos(), upto);
		if (outputCursor->Pos() > maxLogSize)
			Wrap();
		SetSelection(inputCursor->Pos(), inputCursor->End());
		delete gt;
	}
	if (doReveal) {
		int p= outputCursor->Pos();
		Rectangle r= BoundingRect(p, p).Expand(8);
		RevealOutput(r, r.extent);
	}
	outputCursor->Lock();
	PerformCommand(gResetUndo);
	inReceive= FALSE;
	FlushPendingInput();
}

void ShellTextView::FlushPendingInput()
{
	if (inpQueue->Size()) {
		ByteArray *bp;
		Iter next(inpQueue);
		while (bp= (ByteArray*)next()) {
			TtyInput((char*)bp->Str());
			inpQueue->RemovePtr(bp);
			SafeDelete(bp);
		}
	}
}

void ShellTextView::InsertReceivedText(Text *t, int from, int to)
{
	if (!text->IsKindOf(StyledText))
		text->Paste(t, from, to);
	else {
		StyledText *st= (StyledText*) text;
		int upto, ostart= outputCursor->Pos();
		st= (StyledText*) text;
		st->SetCharStyle(eTxTPFace, from, to, CharStyleSpec(eFontDefault, eFacePlain, 0));
		text->Paste(t, from, to);
		if (ostart <= lastSubmit && !ptty->IsRawOrCBreak()) {
			upto= Math::Min(Math::Min(lastSubmit, outputCursor->Pos()), text->End());
			st->SetCharStyle(eTxTPFace, ostart, upto, CharStyleSpec(eFontDefault, eFaceBold,0));
			st->SetCharStyle(eTxTPFace, upto, upto, CharStyleSpec(eFontDefault, eFacePlain,0));
		}
	}
}

void ShellTextView::ProcessOutput(GapText *gt, char *buf, int n)
{
	for (char *p= buf; p < buf+n; p++)
		if (HandleSpecialOutChar(*p, gt))
			gt->AddChar(gt->End(), *p);
}

void ShellTextView::RevealOutput(const Rectangle &r, const Point &p)
{
	RevealRect(r, p);
}

bool ShellTextView::HandleSpecialOutChar(char p, GapText *t)
{
	if (state) {
		state= FALSE;
		return FALSE;
	}
	if (pendingCR) {
		if (p == '\r')
			return FALSE;
		if (p == '\n')
			pendingCR= FALSE;
		else {
			CarriageReturn(t);
			pendingCR= FALSE;
		}
	}

	switch (p) {
	case cESC: // during a more(1) spurious ESC '9's appear
		state= TRUE;
		return FALSE;
	case '\r':
		pendingCR= TRUE;
		break;
	case '\b':
		BackSpace(t, 1);
		break;
	case '\007':
		gWindow->Bell();
		break;
	default:
		return TRUE;
	}
	return FALSE;
}

void ShellTextView::Submit(const char *buf, int l)
{
	if (l < 0)
		l= strlen(buf);
	if (l && ptty->IsRunning() && ptty->Write(buf, l) < 0)
		ShowAlert(eAlertNote, "Pty command buffer overflow\nlast command ignored");
	lastSubmit= text->End();
}

void ShellTextView::SubmitInterrupt(char ch)
{
	if (ptty->IsRunning())
		ptty->Write(&ch, 1);
}

void ShellTextView::Interrupt()
{
	SubmitInterrupt(ptty->CCIntr());
}

void ShellTextView::Done()
{
	if (ptty->IsRunning())
		ptty->Kill();
}

bool ShellTextView::IsIdle()
{
	return !ptty->IsRunning();
}

void ShellTextView::SlaveDied()
{
	if (ptty->IsRunning())
		CleanupDeath();
}

void ShellTextView::CleanupDeath()
{
	ClearOutput();
	ptty->Kill();
}

void ShellTextView::DrawOutputCursor(const Point &p)
{
	Bitmap *bm= gRevCaretImage;
	Point caretsize(bm->Size());
	Rectangle r(p+Point(-caretsize.y, 0), caretsize);
	r.origin+= GetInnerOrigin();

	if (gCaretInk == 0) {
		if (gColor)
			gCaretInk= new_RGBColor(0, 0, 255);
		else
			gCaretInk= ePatXor;
	}
	GrPaintBitMap(r, bm, gCaretInk);
}

void ShellTextView::Draw(Rectangle r)
{
	Point p;
	int l;
//    if (!ptty)
//        GrPaintRect(r, ePatGrey25);
	TextView::Draw(r);
	if (ptty->IsRunning() && ptty->IsRawOrCBreak())
		return;
	CharToPoint(outputCursor->Pos(), &l, &p);
	DrawOutputCursor(p);
}

void ShellTextView::Paste(Text *insert)
{
	if (ModifiesReadOnly())
		return;
	if (ptty->IsRunning() && ptty->IsRawOrCBreak()) {
		AutoTextIter next(insert);
		Token t;
		int ch;
		while ((ch= next()) != cEOT) {
			t.Code= (short)ch;
			DoKeyCommand((short)ch, t);
		}
	} else
		TextView::Paste(insert);
}

bool ShellTextView::ModifiesReadOnly()
{
	int f;
	GetSelection(&f);
	return f < outputCursor->Pos();
}

bool ShellTextView::DeleteRequest(int from, int)
{
	return from >= outputCursor->Pos();
}

void ShellTextView::BecomeConsole()
{
	if (ptty->IsRunning())
		ptty->BecomeConsole();
}

void ShellTextView::BackSpace(Text *t, int n)
{
	int s= t->End();
	if (s > 0) {
		int d1= Math::Min(s, n);
		t->Cut(s-d1, s);
	}
	if (n > s) {
		int d2= n-s;
		lastSubmit= Math::Max(0, lastSubmit-d2);
		int p= outputCursor->Pos();
		outputCursor->ChangeMark(Math::Max(0, p-d2), 0);
		text->Cut(p-d2, p);
	}
}

void ShellTextView::CarriageReturn(Text *t)
{
	register int at, s, ch;
	pendingCR= FALSE;
	for (s= t->End(); s > 0; s--) {
		ch= (*t)[s];
		if (ch == '\n' || ch == '\r') {
			t->Cut(s+1, t->End());
			return;
		}
	}
	t->Empty();
	s= text->End();
	for (at= s-1; at >= 0; at--) {
		ch= (*text)[at];
		if (ch == '\n' || ch == '\r') {
			outputCursor->ChangeMark(at+1, 0);
			text->Cut(at+1, s);
			lastSubmit= at+1;
			return;
		}
	}
}

Command *ShellTextView::CntrlChar(byte b)
{
	GapText gt((byte*)"^");
	gt.Append(b+'@');
	return InsertText(&gt);
}

void ShellTextView::Wrap()
{
	int del= StartLine(CharToLine(Math::Max(0, text->End()-cLowWaterMark)));
	lastSubmit-= del;
	text->Cut(0, del);
}

Text *ShellTextView::SetText(Text *t, bool revealTop)
{
	Text *ot= TextView::SetText(t, revealTop);
	if (ot) {
		ot->RemoveMark(outputCursor);
		ot->RemoveMark(inputCursor);
	}
	t->AddMark(outputCursor);
	t->AddMark(inputCursor);
	MarksToEOT();
	return ot;
}

void ShellTextView::MarksToEOT()
{
	Text *t= GetText();
	outputCursor->ChangeMark (t->End(), 0);
	outputCursor->Lock();
	inputCursor->ChangeMark (t->End(), 0);
	lastSubmit= t->End();
}

void ShellTextView::ClearOutput()
{
	text->Cut(0, text->End());
	MarksToEOT();
	SetSelection(0,0,FALSE);
	RevealSelection();
}

void ShellTextView::DoReveal(bool b)
{
	if (!doReveal && b)
		RevealSelection();
	doReveal= b;
}

void ShellTextView::Start(const char *name, const char **args, bool cons)
{
	if (ptty->IsRunning())
		Error("Start", "shell is already running");

	state= 0;
	MarksToEOT();
	if (name == 0)
		name= gDefaultArgv[0];
	argv= args;
	if (args == 0)
		argv= gDefaultArgv;
	if (ptty->Run(name, argv)) {
		if (cons)
			BecomeConsole();
		strfreplace(&pttycstr, "\n\r%c%c%c%c%c",
				ptty->CCReprint(),
				ptty->CCSuspend(),
				ptty->CCIntr(),
				ptty->CCQuit(),
				ptty->CCEof());
		SetStopChars(pttycstr);
	} else
		Error("Start", "could not spawn slave pseudo tty");
}

void ShellTextView::Reconnect()
{
	if (ptty->IsRunning()) {
		switch (ShowAlert(eAlertCaution, "There is alreay a shell connection.\nForce reconnect?")) {
		case cIdYes:
			break;
		case cIdNo:
		case cIdCancel:
			return;
		}
	}
	ptty->Kill();
	Start(argv[0], argv);
	ForceRedraw();
}

Menu *ShellTextView::MakeMenu(int menuId)
{
	if (menuId == cSHELLMENU) {
		Menu *m= new Menu(cSHELLMENU, "Shell");
		m->AppendItems(
			"Auto Reveal On",   cAUTOREVEAL,
			"Reconnect",        cRECONNECT,
			"Become Console",   cBECOMECONSOLE,
			"Do It",            cDOIT,
			0);
		return m;
	}
	return TextView::MakeMenu(menuId);
}

void ShellTextView::DoSetupMenu(Menu *m)
{
	TextView::DoSetupMenu(m);
	if (ModifiesReadOnly()) {
		m->DisableItem(cCUT);
		m->DisableItem(cPASTE);
	}
	m->ReplaceItem(cAUTOREVEAL, doReveal ? "Auto Reveal Off"
					     : "Auto Reveal On");
	if (ptty && ptty->IsRunning())
		m->EnableItem(cBECOMECONSOLE);
	m->EnableItem(cRECONNECT);
	m->EnableItems(cCLEAR, cAUTOREVEAL, 0);
	if (!Caret())
		m->EnableItem(cDOIT);
}

Command *ShellTextView::DoMenuCommand(int cmd)
{
	switch (cmd) {

	case cAUTOREVEAL:
		DoReveal(!doReveal);
		break;

	case cCLEAR:
		ClearOutput();
		return gResetUndo;
		
	case cRECONNECT:
		Reconnect();
		break;
		
	case cBECOMECONSOLE:
		BecomeConsole();
		break;

	case cDOIT:
		Doit();
		return gResetUndo;
	}
	return TextView::DoMenuCommand(cmd);
}

void ShellTextView::SelectAll(bool redraw)
{
	SetSelection(outputCursor->Pos(), text->End(), redraw);
	RevealSelection();
}

void ShellTextView::Doit()
{
	int f, t;
	GapText *buf= new GapText(256);
	Token token;

	GetSelection(&f, &t);
	text->Copy(buf, f, t);
	SetSelection(text->End(), text->End());
	Paste(buf);
	DoKeyCommand('\n', token);
	delete buf;
}

void ShellTextView::SetTtySize(const Rectangle &r, Font *fd)
{
	Metric m(MeasureText(0, fd, r.extent.x, 1));
	SetTtySize(r.extent.y / m.Height(), r.extent.x / fd->Width(' '));
}

void ShellTextView::ChangedViewRect(const Rectangle &r)
{
	SetTtySize(r, text->GetFont(0));
	if (Width() != r.Width())
		SetWidth(r.Width());
}

void ShellTextView::SetTtySize(int rows, int cols)
{
	if (ptty->IsRunning())
		ptty->SetSize(Point(cols, rows));
}

void ShellTextView::SetMaxLogSize(int n)
{
	maxLogSize= n;
}

Command *ShellTextView::TtyInput(const char *buf, int len)
{
	if (inReceive) {
		inpQueue->Add(new ByteArray(buf, len));
		return gNoChanges;
	}
	if (len == -1)
		len= strlen(buf);

	if (!ptty->IsRunning())
		return gNoChanges;

	Token t;
	Command *cmd= gResetUndo;
	if (ModifiesReadOnly())
		SetSelection(text->End(), text->End(), TRUE);
	if (ptty->IsRawOrCBreak() || !ptty->IsEcho()) {
		for (const char *cp= buf; cp < buf+len; cp++) {
			t.Code= (short)*cp;
			cmd= DoKeyCommand((int)*cp, t);
		}
	} else {
		// batch submitted characters
		const char *s= buf, *e= buf;
		Text *scratchText= text->MakeScratchText(0, 80);

		for (;;) {
			while (e < buf+len && !strchr(pttycstr, *e) && !Iscntrl(*e)) {
				scratchText->Append(*e);
				e++;
			}
			InsertText(scratchText);
			scratchText->Empty();
			t.Code= (short)*e;
			cmd= DoKeyCommand((int)*e, t);
			e++;
			s= e;
			if (s >= buf+len)
				break;
		}
		SafeDelete(scratchText);
		SetSelection(text->End(), text->End(), TRUE);
		RevealSelection();
	}
	return cmd;
}

OStream& ShellTextView::PrintOn(OStream &s)
{
	TextView::PrintOn(s);
	return s << doReveal SP;
}

IStream& ShellTextView::ReadFrom(IStream &s)
{
	TextView::ReadFrom(s);
	Term();
	s >> Bool(doReveal);
	Init(doReveal, gDefaultArgv[0], gDefaultArgv, FALSE);
	MarksToEOT();
	return s;
}

